/*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package com.android.utils.traversal;

import android.graphics.Rect;
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
import android.util.Log;
import com.android.utils.AccessibilityNodeInfoUtils;
import com.android.utils.LogUtils;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * Calculates the utility bounds of the node. If node is not supposed to get accessibility focus
 * the utility bounds is calculated on the base of minimum rect that contains all accessibility
 * focusable nodes inside node hierarchy rooted by this node.
 * WARNING: Calculator does not obtain or recycle nodes for performance reason.
 * If any of the nodes that was calculated is recycled the calculator could get wrong results
 */
public class NodeCachedBoundsCalculator {

    private static final Rect EMPTY_RECT = new Rect();

    private Map<AccessibilityNodeInfoCompat, Rect> mBoundsMap = new HashMap<>();
    private Map<AccessibilityNodeInfoCompat, Boolean> mSpeakNodesCache;
    private Set<AccessibilityNodeInfoCompat> mCalculatingNodes = new HashSet<>();
    private Rect mTempRect = new Rect();

    public void setSpeakNodesCache(Map<AccessibilityNodeInfoCompat, Boolean> speakNodeCache) {
        mSpeakNodesCache = speakNodeCache;
    }

    public Rect getBounds(AccessibilityNodeInfoCompat node) {
        Rect bounds = getBoundsInternal(node);
        if (bounds == EMPTY_RECT) {
            return null;
        }

        return bounds;
    }

    private Rect getBoundsInternal(AccessibilityNodeInfoCompat node) {
        if (node == null) {
            return EMPTY_RECT;
        }

        if (mCalculatingNodes.contains(node)) {
            LogUtils.log(Log.WARN, "node tree loop detected while calculating node bounds");
            return EMPTY_RECT;
        }

        Rect bounds = mBoundsMap.get(node);
        if (bounds == null) {
            mCalculatingNodes.add(node);
            bounds = fetchBound(node);
            mBoundsMap.put(node, bounds);
            mCalculatingNodes.remove(node);
        }

        return bounds;
    }

    private Rect fetchBound(AccessibilityNodeInfoCompat node) {
        if (node == null || !AccessibilityNodeInfoUtils.isVisible(node)) {
            return EMPTY_RECT;
        }

        if (AccessibilityNodeInfoUtils.shouldFocusNode(node, mSpeakNodesCache)) {
            Rect bounds = new Rect();
            node.getBoundsInScreen(bounds);
            return bounds;
        }

        int childCount = node.getChildCount();
        int minTop = Integer.MAX_VALUE;
        int minLeft = Integer.MAX_VALUE;
        int maxBottom = Integer.MIN_VALUE;
        int maxRight = Integer.MIN_VALUE;
        AccessibilityNodeInfoCompat child = null;
        boolean hasChildBounds = false;
        for (int i = 0; i < childCount; i++) {
            try {
                child = node.getChild(i);
                Rect bounds = getBoundsInternal(child);
                if (bounds != EMPTY_RECT) {
                    hasChildBounds = true;
                    if (bounds.top < minTop) {
                        minTop = bounds.top;
                    }

                    if (bounds.left < minLeft) {
                        minLeft = bounds.left;
                    }

                    if (bounds.right > maxRight) {
                        maxRight = bounds.right;
                    }

                    if (bounds.bottom > maxBottom) {
                        maxBottom = bounds.bottom;
                    }
                }
            } finally {
                AccessibilityNodeInfoUtils.recycleNodes(child);
            }
        }

        Rect bounds = new Rect();
        node.getBoundsInScreen(bounds);
        if (hasChildBounds) {
            bounds.top = Math.max(minTop, bounds.top);
            bounds.left = Math.max(minLeft, bounds.left);
            bounds.right = Math.min(maxRight, bounds.right);
            bounds.bottom = Math.min(maxBottom, bounds.bottom);
        }

        return bounds;
    }

    /**
     * If node is not supposed to be accessibility focused by TalkBack
     * NodeBoundsCalculator calculates useful bounds of focusable children.
     * The method checks if the node uses its children useful bounds or uses its own bounds
     */
    public boolean usesChildrenBounds(AccessibilityNodeInfoCompat node) {
        if (node == null) {
            return false;
        }

        Rect bounds = getBounds(node);
        if (bounds == null) {
            return false;
        }

        node.getBoundsInScreen(mTempRect);
        return !mTempRect.equals(bounds);
    }
}
